/* This code is restricted under LGPL license */
using System;
using NHibernate.Criterion;
using System.Reflection;

namespace infoMECHANICA.NHibernateORM.CriteriaBuilder
{        
    /// <summary>
    /// Recorders base class
    /// </summary>
    /// <typeparam name="R">The type of this recorder</typeparam>
    /// <typeparam name="Dto">The type of DTO object being wrapped by this recorder</typeparam>
    public class CriteriaRecorder<R, Dto> : ICriteriaRecorder where R : ICriteriaRecorder
    {
        #region ICriteriaRecorder Members
        private ICriteriaRecorder __parentRecorderReference;
        private bool hasSubreference;

        public void SetParent(ICriteriaRecorder parent)
        {
            this.__parentRecorderReference = parent;
        }

        public ICriteriaRecorder GetParent()
        {
            return this.__parentRecorderReference;
        }

        /// <summary>
        /// <see>ICriteriaRecorder</see> interface summary
        /// </summary>
        /// <returns></returns>
        public bool HasSubreference
        {
            get { return hasSubreference; }
            set { hasSubreference = value; }
        }
        
        /// <summary>
        /// <see>ICriteriaRecorder</see> interface summary
        /// </summary>
        /// <returns></returns>
        public virtual string GetIdPropertyName()
        {
            return "Id";
        }
        #endregion

        /// <summary>
        /// Recorder has any criterion set?
        /// </summary>
        public bool HasCriteria()
        {
            PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
            foreach (PropertyInfo property in properties)
            {                
                // check whether recorder has set any criteria
                object propertyValue = property.GetValue(this, null);
                if (propertyValue is ICriterionSource)
                {
                    ICriterionSource criterionSource = (ICriterionSource)property.GetValue(this, null);
                    ICriterion criterionToAdd = criterionSource.GetCriterion();
                    if (criterionToAdd != null)
                    {
                        return true;
                    }
                }
                else
                {
                    // check whether any of subrecorders has criteria
                    if (propertyValue is ICriteriaRecorder)
                    {
                        if ((((ICriteriaRecorder)propertyValue).HasSubreference) && ((ICriteriaRecorder)propertyValue).HasCriteria())
                        {
                            return true;
                        }
                    }
                }
            }

            return false;
        }
        
        /// <summary>
        /// Decorates criteria with criterions (based on Recorder properties)
        /// </summary>
        /// <param name="criteria">Referenced criteria</param>
        /// <param name="recorder">Recorder of criterions</param>
        private void ComposeCriteria(DetachedCriteria criteria, ICriteriaSource recorder)
        {
            PropertyInfo[] properties = recorder.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
            foreach (PropertyInfo property in properties)
            {                
                object source = property.GetValue(recorder, null);

                // ...simple queries
                if (source is ICriterionSource)
                {
                    ICriterionSource criterionSource = (ICriterionSource)property.GetValue(recorder, null);
                    ICriterion criterionToAdd = criterionSource.GetCriterion();
                    if (criterionToAdd != null)
                    {
                        criteria.Add(criterionToAdd);
                    }
                }                                   
                // ... many-to-many relationships
                else if (source is ISubcriteriaSource)
                {
                    //WARNING!!!
                    //every ISubcriteriaSource is ICriteriaSource as well so it must be checked before ICriteriaSource properties
                    string newAliasName = String.Format(@"{0}_{1}", property.Name, criteria.Alias);
                    if (((ISubcriteriaSource)source).HasCriteria())
                    {
                        string masterId = String.Format("{0}.{1}", criteria.Alias, this.GetIdPropertyName());
                        string slaveId = newAliasName + "." + ((ISubcriteriaSource)source).MasterPropertyName;
                        DetachedCriteria subcriteria = ((ISubcriteriaSource)source).BuildCriteria(newAliasName);
                        subcriteria.SetProjection(Projections.Id());
                        subcriteria.Add(Property.ForName(slaveId).EqProperty(masterId));
                        
                        switch (((ISubcriteriaSource)source).SubqueryConjuntionType)
                        {
                            case SubqueryConjunctionTypes.Exists:
                                criteria.Add(Subqueries.Exists(subcriteria));
                                break;
                            case SubqueryConjunctionTypes.NotExists:
                                criteria.Add(Subqueries.NotExists(subcriteria));
                                break;
                        }                        
                    }                    
                }
                // ...one-to-many relationships
                else if (source is ICriteriaSource)
                {
                    string newAliasName = String.Format("{0}_{1}", criteria.Alias, property.Name);
                    if (((ICriteriaSource)source).HasCriteria())
                    {
                        DetachedCriteria subCriteria = criteria.CreateCriteria(property.Name, newAliasName, NHibernate.SqlCommand.JoinType.InnerJoin);
                        ComposeCriteria(subCriteria, (ICriteriaSource)source);
                    }
                } 
            }            
        }
  
        /// <summary>
        /// Builds NHibernate detached criteria using standard criteria alias name: "root"
        /// </summary>
        /// <returns>NHibernate detached criteria</returns>
        public DetachedCriteria BuildCriteria()
        {
            return BuildCriteria("root");
        }

        /// <summary>
        /// Builds NHibernate detached criteria and creates alias for returned criteria
        /// </summary>
        /// <param name="alias">Alias of detached criteria being created</param>
        /// <returns></returns>
        public DetachedCriteria BuildCriteria(string alias)
        {
            DetachedCriteria criteria = DetachedCriteria.For(typeof(Dto), alias);
            ComposeCriteria(criteria, this);            

            return criteria;
        }

        #region Operators overloading
        /// <summary>
        /// Merges two Recorders together as a conjunction
        /// </summary>
        /// <param name="recorder1">First Recorder to be merged</param>
        /// <param name="recorder2">Second Recorder to be merged</param>
        /// <returns>Merged MergeSet</returns>
        public static MergeSet<R, Dto> operator &(CriteriaRecorder<R, Dto> recorder1,
                          CriteriaRecorder<R, Dto> recorder2)
        {
            return recorder1.ToMergeSet() & recorder2.ToMergeSet();
        }

        /// <summary>
        /// Merges MergeSet and Recorder together as a conjunction
        /// </summary>
        /// <param name="set">MergeSet to be merged</param>
        /// <param name="recorder">Recorder to be merged</param>
        /// <returns>Merged MergeSet</returns>
        public static MergeSet<R, Dto> operator &(MergeSet<R, Dto> set, CriteriaRecorder<R, Dto> recorder)
        {
            return set & recorder.ToMergeSet();
        }

        /// <summary>
        /// Merges MergeSet and Recorder together as a conjunction
        /// </summary>
        /// <param name="set">MergeSet to be merged</param>
        /// <param name="recorder">Recorder to be merged</param>
        /// <returns>Merged MergeSet</returns>
        public static MergeSet<R, Dto> operator &(CriteriaRecorder<R, Dto> recorder, MergeSet<R, Dto> set)
        {
            return recorder.ToMergeSet() & set;
        }


        /// <summary>
        /// Merges two Recorders together as a disjunction
        /// </summary>
        /// <param name="recorder1">First Recorder to be merged</param>
        /// <param name="recorder2">Second Recorder to be merged</param>
        /// <returns>MergeSet object with two recorders merged</returns>
        public static MergeSet<R, Dto> operator |(CriteriaRecorder<R, Dto> recorder1,
                          CriteriaRecorder<R, Dto> recorder2)
        {
            return (recorder1.ToMergeSet() | recorder2.ToMergeSet());
        }

        /// <summary>
        /// Merges MergeSet and Recorder together as a disjunction
        /// </summary>
        /// <param name="set">MergeSet to be merged</param>
        /// <param name="recorder">Recorder to be merged</param>
        /// <returns>Merged MergeSet</returns>
        public static MergeSet<R, Dto> operator |(MergeSet<R, Dto> set, CriteriaRecorder<R, Dto> recorder)
        {
            return set | recorder.ToMergeSet();
        }

        /// <summary>
        /// Merges MergeSet and Recorder together as a disjunction
        /// </summary>
        /// <param name="set">MergeSet to be merged</param>
        /// <param name="recorder">Recorder to be merged</param>
        /// <returns>Merged MergeSet</returns>
        public static MergeSet<R, Dto> operator |(CriteriaRecorder<R, Dto> recorder, MergeSet<R, Dto> set)
        {
            return recorder.ToMergeSet() | set;
        }

        /// <summary>
        /// Converts recorder to MergeSet object
        /// </summary>
        /// <returns>MergeSet</returns>
        public MergeSet<R, Dto> ToMergeSet()
        {
            DetachedCriteria criteria = this.BuildCriteria();
            return new MergeSet<R, Dto>(criteria, this.GetIdPropertyName());
        } 
        #endregion

        /// <summary>
        /// Constructor, creates CriteriaRecorder with no parent
        /// </summary>
        public CriteriaRecorder()
        {            
        }

        /// <summary>
        /// Constuctor, creates CriteriaRecorder and sets its parent
        /// </summary>
        /// <param name="parent">Parent of this recorder</param>
        public CriteriaRecorder(ICriteriaRecorder parent)
        {
            this.SetParent(parent);
        }        
    }
}